package repository

import (
	"context"
	"net/http"
	"server/components/errors"
	"server/components/logger"
	"server/components/models/ent"
	"server/components/models/ent/predicate"
	"server/components/models/ent/region"
	"server/components/models/ent/weather"
	"time"
)

func GetWeatherByRegionId(ctx context.Context, regionId int64) (*Weather, errors.RequestError) {
	if v, err := EntClient().Region.Query().Where(region.ID(regionId)).Exist(ctx); err != nil || !v {
		return nil, errors.New("Регион не найден", http.StatusNotFound)
	}
	w, err := EntClient().Weather.Query().Where(
		weather.And(
			weather.HasRegionWith(
				region.ID(regionId),
			),
		),
	).
		WithForecast(
			func(fq *ent.ForecastQuery) {
				fq.WithRegion()
			},
		).
		WithRegion().
		Only(ctx)
	if ent.IsNotFound(err) {
		return nil, errors.New("Погода для данного региона не найдена", http.StatusNotFound)
	}
	forecasts := make([]Forecast, 0, 1)
	for _, f := range w.Edges.Forecast {
		if f.Edges.Region.ID == regionId {
			forecasts = append(forecasts, Forecast{
				ID:                  f.ID,
				RegionId:            regionId,
				Temperature:         f.Temperature,
				WeatherCondition:    string(f.WeatherCondition),
				PrecipitationAmount: f.PrecipitationAmount,
				WindSpeed:           f.WindSpeed,
				DateTime:            f.DateTime.Format(time.RFC3339),
			})
		}
	}
	return &Weather{
		ID:                  regionId,
		Temperature:         w.Temperature,
		Humidity:            w.Humidity,
		WindSpeed:           w.WindSpeed,
		WeatherCondition:    w.WeatherCondition.String(),
		PrecipitationAmount: w.PrecipitationAmount,
		MeasurementDateTime: w.MeasurementDateTime.Format(time.RFC3339),
		RegionName:          w.Edges.Region.Name,
		WeatherForecast:     forecasts,
	}, nil
}

func GetWeatherById(ctx context.Context, id int64) (*Weather, errors.RequestError) {
	w, err := EntClient().Weather.Query().Where(weather.ID(id)).WithForecast(
		func(fq *ent.ForecastQuery) {
			fq.WithRegion()
		},
	).WithRegion().Only(ctx)
	if ent.IsNotFound(err) {
		return nil, errors.New("Погода с таким ID не найдена", http.StatusNotFound)
	} else if err != nil {
		return nil, errors.MsgWrap("Ошибка получения погоды", err, http.StatusInternalServerError)
	}
	forecasts := make([]Forecast, 0, 1)
	for _, f := range w.Edges.Forecast {
		forecasts = append(forecasts, Forecast{
			ID:                  f.ID,
			RegionId:            f.Edges.Region.ID,
			Temperature:         f.Temperature,
			WeatherCondition:    string(f.WeatherCondition),
			PrecipitationAmount: f.PrecipitationAmount,
			WindSpeed:           f.WindSpeed,
			DateTime:            f.DateTime.Format(time.RFC3339),
		})
	}
	return &Weather{
		ID:                  w.ID,
		Temperature:         w.Temperature,
		Humidity:            w.Humidity,
		WindSpeed:           w.WindSpeed,
		WeatherCondition:    w.WeatherCondition.String(),
		PrecipitationAmount: w.PrecipitationAmount,
		MeasurementDateTime: w.MeasurementDateTime.Format(time.RFC3339),
		RegionName:          w.Edges.Region.Name,
		WeatherForecast:     forecasts,
	}, nil
}

func SearchWeatherByRegion(ctx context.Context, request SearchWeatherRequest) ([]WeatherSearchResult, errors.RequestError) {
	predicates := make([]predicate.Weather, 0, 1)
	if request.RegionId != nil {
		predicates = append(predicates,
			weather.HasRegionWith(
				region.ID(*request.RegionId),
			),
		)
	}
	if request.StartDateTime != nil {
		if t, err := ParseTime(*request.StartDateTime); err != nil {
			return nil, errors.New("Некорректный формат даты", http.StatusBadRequest)
		} else {
			predicates = append(predicates, weather.MeasurementDateTimeGTE(t))
		}
	}
	if request.EndDateTime != nil {
		if t, err := ParseTime(*request.EndDateTime); err != nil {
			return nil, errors.New("Некорректный формат даты", http.StatusBadRequest)
		} else {
			predicates = append(predicates, weather.MeasurementDateTimeLTE(t))
		}
	}
	if request.WeatherCondition != nil {
		if err := weather.WeatherConditionValidator(weather.WeatherCondition(*request.WeatherCondition)); err != nil {
			return nil, errors.New("Некорректный формат состояния погоды", http.StatusBadRequest)
		} else {
			predicates = append(predicates, weather.WeatherConditionEQ(weather.WeatherCondition(*request.WeatherCondition)))
		}
	}
	vec, err := EntClient().Weather.Query().Where(weather.And(predicates...)).
		WithForecast(
			func(fq *ent.ForecastQuery) {
				fq.WithRegion()
			},
		).
		WithRegion().Offset(request.From).Limit(request.Size).All(ctx)
	if ent.IsNotFound(err) || len(vec) == 0 {
		return nil, errors.New("Погода для данного региона не найдена", http.StatusNotFound)
	}
	result := make([]WeatherSearchResult, 0, len(vec))
	for _, w := range vec {
		forecasts := make([]int64, 0, 1)
		for _, f := range w.Edges.Forecast {
			forecasts = append(forecasts, f.ID)
		}
		weather := WeatherSearchResult{
			ID:                  w.Edges.Region.ID,
			Temperature:         w.Temperature,
			Humidity:            w.Humidity,
			WindSpeed:           w.WindSpeed,
			WeatherCondition:    w.WeatherCondition.String(),
			PrecipitationAmount: w.PrecipitationAmount,
			MeasurementDateTime: w.MeasurementDateTime.Format(time.RFC3339),
			RegionName:          w.Edges.Region.Name,
			WeatherForecast:     forecasts,
		}
		result = append(result, weather)
	}
	return result, nil
}

func AddWeather(ctx context.Context, req AddWeatherRequest) (*Weather, errors.RequestError) {
	region, rErr := GetRegionByID(ctx, req.RegionId)
	if rErr != nil {
		return nil, rErr
	}
	dt, err := ParseTime(req.MeasurementDateTime)
	if err != nil {
		return nil, errors.New("Неверный формат даты", http.StatusBadRequest)
	}
	w, err := EntClient().Weather.Create().
		SetHumidity(req.Humidity).
		SetMeasurementDateTime(dt).
		SetPrecipitationAmount(req.PrecipitationAmount).
		SetRegionID(region.ID).
		SetTemperature(req.Temperature).
		SetWeatherCondition(weather.WeatherCondition(req.WeatherCondition)).
		SetWindSpeed(req.WindSpeed).
		AddForecastIDs(req.WeatherForecast...).Save(ctx)
	if err != nil {
		return nil, errors.New("Неверные данные", http.StatusBadRequest)
	}
	return GetWeatherById(ctx, w.ID)
}

func UpdateWeather(ctx context.Context, regionId int64, req UpdateWeatherRequest) (*Weather, errors.RequestError) {
	tx, err := EntClient().Tx(ctx)
	if err != nil {
		logger.Logger().Errorf("cannot start transaction %v", err)
		return nil, errors.New("Невозможно создать транзакцию", http.StatusInternalServerError)
	}
	defer tx.Rollback()
	_, rErr := GetRegionByID(ctx, regionId)
	if rErr != nil {
		return nil, rErr
	}
	w, err := tx.Weather.Query().Where(
		weather.HasRegionWith(
			region.ID(regionId),
		),
	).Only(ctx)
	if ent.IsNotFound(err) {
		return nil, errors.New("Погода для данного региона не найдена", http.StatusNotFound)
	} else if err != nil {
		logger.Logger().Errorf("Cannot get weather %v", err)
		return nil, errors.New("Ошибка проверки погоды для региона", http.StatusBadRequest)
	}
	if req.RegionName != nil {
		if err := tx.Region.UpdateOneID(regionId).SetName(*req.RegionName).Exec(ctx); err != nil {
			return nil, errors.New("Невозможно обновить имя региона", http.StatusBadRequest)
		}
	}
	update := tx.Weather.UpdateOneID(w.ID)
	if req.MeasurementDateTime != nil {
		dt, err := ParseTime(*req.MeasurementDateTime)
		if err != nil {
			return nil, errors.New("Неверный формат даты", http.StatusBadRequest)
		}
		update = update.SetMeasurementDateTime(dt)
	}
	if req.Humidity != nil {
		update = update.SetHumidity(*req.Humidity)
	}
	if req.PrecipitationAmount != nil {
		update = update.SetPrecipitationAmount(*req.PrecipitationAmount)
	}
	if req.Temperature != nil {
		update = update.SetTemperature(*req.Temperature)
	}
	if req.WeatherCondition != nil {
		update = update.SetWeatherCondition(weather.WeatherCondition(*req.WeatherCondition))
	}
	if req.WeatherForecast != nil {
		update = update.
			ClearForecast().
			AddForecastIDs(req.WeatherForecast...)
	}
	if req.WindSpeed != nil {
		update = update.SetWindSpeed(*req.WindSpeed)
	}
	if _, err := update.Save(ctx); err != nil {
		return nil, errors.New("Невозможно обновить погоду для региона", http.StatusBadRequest)
	}
	if err := tx.Commit(); err != nil {
		logger.Logger().Errorf("Cannot commit transaction %v", err)
		return nil, errors.New("Невозможно обновить погоду для региона", http.StatusBadRequest)
	}
	return GetWeatherById(ctx, w.ID)
}

func DeleteWeatherForRegion(ctx context.Context, regionId int64) errors.RequestError {
	n, err := EntClient().Weather.Delete().Where(weather.HasRegionWith(region.ID(regionId))).Exec(ctx)
	if err != nil {
		return errors.New("Невозможно удалить погоду из региона", http.StatusBadRequest)
	}
	if n == 0 {
		return errors.New("Невозможно удалить погоду из не существующего региона", http.StatusNotFound)
	}
	return nil
}

func SetRegionWeather(ctx context.Context, regionId int64, weatherId int64) (*Weather, errors.RequestError) {
	_, err := EntClient().Weather.UpdateOneID(weatherId).SetRegionID(regionId).Save(ctx)
	if err != nil {
		logger.Logger().Errorf("Cannot bind weather to region %v", err)
		return nil, errors.New("Регион или погода не найдены", http.StatusNotFound)
	}
	return GetWeatherById(ctx, weatherId)
}

func UnsetRegionWeather(ctx context.Context, regionId int64, weatherId int64) (*Region, errors.RequestError) {
	_, err := EntClient().Weather.UpdateOneID(weatherId).ClearRegion().Save(ctx)
	if err != nil {
		logger.Logger().Errorf("Cannot unbind weather from region %v", err)
		return nil, errors.New("Регион или погода не найдены", http.StatusNotFound)
	}
	return GetRegionByID(ctx, regionId)
}
